package org.jenkinsci.plugins.p4scm.utils;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import hudson.EnvVars;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Node;
import hudson.model.TaskListener;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

import jenkins.model.Jenkins;

import org.jenkinsci.plugins.p4scm.P4SCM;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MacroStringHelper {
    
    @SuppressWarnings("unused")
    private static final Logger logger = LoggerFactory.getLogger(MacroStringHelper.class);

    /**
     * Substitute parameters and validate contents of the resulting string
     * @param string Input string to be substituted
     * @param instance Instance of {@link P4SCM}
     * @param build A build to be substituted
     * @param env Additional environment variables.
     * @return Substituted string. May be null if the input string is null
     * @throws InterruptedException 
     * @throws ParameterSubstitutionException Format error (unresolved variable, etc.)
     */
    public static String substituteParameters(
            @CheckForNull String string,
            @Nonnull P4SCM instance,
            @Nonnull AbstractBuild<?, ?> build, 
            @CheckForNull Map<String, String> env) throws InterruptedException, ParameterSubstitutionException {
        if (string == null) return null;
        String result = substituteParametersNoCheck(string, instance, build, env);
        checkString(result);
        return result;
    }
    
    /**
     * Substitute parameters and validate contents of the resulting string.
     * Environment variables have the highest priority.
     * @param inputString Input string
     * @param instance Instance of {@link PerforceSCM}
     * @param build Related build
     * @param env Additional environment variables
     * @return Substituted string
     */        
    private static String substituteParametersNoCheck(
            @Nonnull String inputString,
            @Nonnull P4SCM instance,
            @Nonnull AbstractBuild<?, ?> build, 
            @CheckForNull Map<String, String> env) throws InterruptedException {
        
        // There is no Marco in 
        if (!containsMacro(inputString)) {
            return inputString;
        }
        
        String result = inputString;
        
        // Substitute variables with custom values (escaping, etc)
        final Map<String, String> customVars = new TreeMap<String, String>();
        customVars.put("JOB_NAME", JobSubstitutionHelper.getSafeJobName(build));
        result = MacroStringHelper.substituteParametersNoCheck(result, customVars);
        
     // Try to build the full environment. Nested calls count is handled in PerforceSCM::buildEnvVars() 
        Map<String, String> environmentVarsFromExtensions = new TreeMap<String, String>();
        try {
            EnvVars vars = build.getEnvironment(TaskListener.NULL);
            environmentVarsFromExtensions.putAll(vars);
        } catch (IOException ex) {
            logger.error(null, ex);
        }
        result = MacroStringHelper.substituteParametersNoCheck(result, environmentVarsFromExtensions);
              
        // Intermediate check to avoid wasted efforts
        if (!containsMacro(result)) {
            return result;
        }
        
        // Substitute static variables
        result = substituteParametersNoCheck(result, instance, build.getProject(), build.getBuiltOn(), env);
              
        // Substitute default build variables
        Map<String, String> substitutions = new HashMap<String, String>();
        getDefaultBuildSubstitutions(build, substitutions);    
        result = MacroStringHelper.substituteParametersNoCheck(result, substitutions);
        result = MacroStringHelper.substituteParametersNoCheck(result, build.getBuildVariables());
              
        return result;
    }
    
    private static void getDefaultBuildSubstitutions(
            @Nonnull AbstractBuild<?,?> build, 
            @Nonnull Map<String, String> subst) {
        String hudsonName = Jenkins.getInstance().getDisplayName().toLowerCase();
        subst.put("BUILD_TAG", hudsonName + "-" + build.getProject().getName() + "-" + String.valueOf(build.getNumber()));
        subst.put("BUILD_ID", build.getId());
        subst.put("BUILD_NUMBER", String.valueOf(build.getNumber()));
        String rootUrl = Jenkins.getInstance().getRootUrl();
        if (rootUrl != null) {   
            subst.put("BUILD_URL", rootUrl + build.getUrl());
        }
    }
    
    /**
     * Substitute parameters and validate contents of the resulting string
     * @param string Input string
     * @param subst Variables Map
     * @return Substituted string
     */
    private static String substituteParametersNoCheck(
            @CheckForNull String string, 
            @Nonnull Map<String, String> subst) {
        if (string == null) {
            return null;
        }
        String newString = string;
        for (Map.Entry<String, String> entry : subst.entrySet()) {
            final @CheckForNull String key = entry.getKey();
            final @CheckForNull String value = entry.getValue(); 
            if (key == null || value == null) {
                continue;
            }
            newString = newString.replace("${" + key + "}", value);
        }
        return newString;
    }
    
    /**
     * Checks string from unsubstituted variable references.
     * @param string Input string (should be substituted before call).
     *      Null string will be interpreted as OK
     * @throws ParameterSubstitutionException Substitution error
     */
    public static void checkString(@CheckForNull String string) throws ParameterSubstitutionException {
        
        // Conditional fail on substitution error
        if (containsMacro(string)) {
            throw new ParameterSubstitutionException(string, "Found unresolved macro at '" + string + "'");
        }

        //TODO: manage validation by global params?
        //TODO: Check single brackets
        //TODO: Add checks for '$' without brackets 
    }
    
    /**
     * Check if the input string contains macro variables.
     * @param str String to be checked
     * @return true if the string 
     */
    public static boolean containsMacro(@CheckForNull String str) {
        return str != null && str.contains("${");
    }

    public static String substituteParameters(
            @CheckForNull String string,
            @Nonnull P4SCM instance,        
            @CheckForNull AbstractBuild<?,?> build,
            @CheckForNull AbstractProject<?,?> project,
            @CheckForNull Node node,
            @CheckForNull Map<String, String> env)
            throws ParameterSubstitutionException, InterruptedException {
        
        return build != null
                ? substituteParameters(string, instance, build, env)
                : substituteParameters(string, instance, project, node, env);
    }
    
    public static String substituteParameters(
            @CheckForNull String string,
            @Nonnull P4SCM instance,
            @CheckForNull AbstractProject<?,?> project,
            @CheckForNull Node node,
            @CheckForNull Map<String, String> env)
            throws ParameterSubstitutionException, InterruptedException {
        if (string == null) return null;
        String result = substituteParametersNoCheck(string, instance, project, node, env);
        checkString(result);
        return result;
    }
    
    private static String substituteParametersNoCheck (
            @Nonnull String inputString,
            @Nonnull P4SCM instance,
            @CheckForNull AbstractProject<?,?> project,
            @CheckForNull Node node,
            @CheckForNull Map<String, String> env) throws InterruptedException {
        
        if (!containsMacro(inputString)) { // do nothing for the missing macro
            return inputString;
        }
        String outputString = inputString;
        
        // Substitute additional environment vars if possible
        if (env != null && !env.isEmpty()) {
            outputString = substituteParametersNoCheck(outputString, env);
            if (!containsMacro(outputString)) { //exit if no macros left
                return outputString;
            }
        }
                
        // Prepare the substitution container and substitute vars
        Map<String, String> substitutions = new HashMap<String, String>();
        getDefaultCoreSubstitutions(substitutions);
        NodeSubstitutionHelper.getDefaultNodeSubstitutions(instance, node, substitutions);
        if (project != null) { 
            JobSubstitutionHelper.getDefaultSubstitutions(project, substitutions);
        }
        getDefaultSubstitutions(instance, substitutions);
        outputString = substituteParametersNoCheck(outputString, substitutions);    
        
        return outputString;
    }

    private static void getDefaultSubstitutions(
            @Nonnull P4SCM instance,
            @Nonnull Map<String, String> subst) {
        
        subst.put("P4USER", substituteParametersNoCheck(instance.getEffectiveP4User(), subst));
    }

    /**
     * Gets variables of {@link Jenkins} instance.
     */
    private static void getDefaultCoreSubstitutions(@Nonnull Map<String, String> env) {
        String rootUrl = Jenkins.getInstance().getRootUrl();
        if (rootUrl != null) {
            env.put("JENKINS_URL", rootUrl);
            env.put("HUDSON_URL", rootUrl); // Legacy compatibility
        }
        
        String rootPath = Jenkins.getInstance().getRootDir().getPath();
        env.put("JENKINS_HOME", rootPath);
        env.put("HUDSON_HOME", rootPath);   // legacy compatibility
    }

    /**
     * Check if the input string contains the specified variable reference.
     * @param str String to be checked
     * @param variableName Variable name
     * @return true if the string contains the specified variable
     */
    public static boolean containsVariable(
            @CheckForNull String str, 
            @Nonnull String variableName) {
        
        return str != null && str.contains("${" + variableName + "}");
    }

}
